Skip to content

feat: add optional Reddit OAuth credentials and surface real HTTP errors - #6

Merged
eliasbiondo merged 1 commit into
mainfrom
fix/reddit-oauth-and-stdio
Oct 24, 2026
Merged

eliasbiondo merged 1 commit into
mainfrom
fix/reddit-oauth-and-stdio

Conversation

@eliasbiondo

Copy link
Copy Markdown
Owner

Fixes #3

Reddit now answers most unauthenticated programmatic requests with HTTP 403 and an HTML page that says "You've been blocked by network security". I confirmed this from a residential connection, including with browser-like headers. This PR covers all four points in the issue.

1. Optional Reddit OAuth app credentials

New, optional environment variables:

Variable Purpose
REDDIT_CLIENT_ID / REDDIT_CLIENT_SECRET Credentials of a Reddit app. When both are set, requests use Reddit's OAuth API.
REDDIT_USER_AGENT Descriptive User-Agent for API requests. The default is python:reddit-no-auth-mcp-server:v<version> (+repo URL).

When both credentials are set, RedditOAuthHttpAdapter (new) takes the place of redd's HTTP transport inside AsyncRedd. It:

  • gets an app-only token with POST https://www.reddit.com/api/v1/access_token (grant_type=client_credentials, HTTP Basic client auth);
  • caches the token and refreshes it 60 s before expires_in, or after a 401 (the request is retried once);
  • rewrites the www.reddit.com URLs that redd builds to https://oauth.reddit.com and sends Authorization: bearer <token>.

redd keeps building the URLs and parsing the responses, so tool output does not change. Anonymous access remains the default. Setting only one of the two credentials is rejected at startup with a clean usage error (exit code 2).

Shippable without a redd release. redd 0.2.x has no public hook for a custom transport, so the adapter is installed on AsyncRedd._http. That attribute is private, but it is the single place all requests go through. A guard raises a ConfigurationError if a future redd removes it, and the OAuth tests run through the real AsyncRedd, so a breaking redd change would fail CI. A public http=/transport hook in redd would let this drop the private attribute later, but nothing here needs one.

2. The real HTTP status and a useful message instead of HTTP error (0)

The 0 comes from redd: AsyncRedd._get catches every exception and re-raises HttpError(0, url, str(exc)). ReddRedditAdapter now inspects __cause__ and raises the matching domain exception:

  • httpx.HTTPStatusError becomes HttpError(<real status>, url, <short body summary>). HTML block pages are cut down to their first sentence, and JSON errors use their message.
  • A 404 becomes NotFoundError.
  • Connection, DNS, timeout, and proxy failures become NetworkError.
  • A 2xx response that isn't JSON becomes ParseError.
  • Token endpoint failures become AuthenticationError.

McpErrorMapper turns these into actionable messages. The live result on a blocked network:

[reddit_get_subreddit_posts] Reddit refused the request (HTTP 403: You've been blocked by network security). Reddit blocks unauthenticated requests from many networks; set REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET to use Reddit's OAuth API (see the README). If credentials are already configured, the subreddit, post, or user may be private, quarantined, or banned.

401, 429, and 5xx get their own messages, and bad credentials produce Reddit OAuth authentication failed. Reddit rejected the OAuth token request (HTTP 401: Unauthorized). Check REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET.

3. FastMCP banner on stdio

The stdio transport now calls mcp.run(transport="stdio", show_banner=False). With fastmcp 3.1 the banner goes to stderr, not stdout, so it doesn't actually corrupt the JSON-RPC stream. It is still noise, and its box-drawing art can trip up clients and Windows consoles. The HTTP transport keeps the default, and FASTMCP_SHOW_SERVER_BANNER=false (now in the README) hides it there.

4. --help crash on Windows cp1252

Removed the emoji from the argparse description, and a test now asserts that the help text is ASCII.

Also

  • __version__ is read from package metadata. It was hard-coded to 0.1.0, so --version and the default User-Agent were wrong.
  • httpx is now a direct dependency. It was already installed through fastmcp.
  • README: dropped the unconditional "no API keys required" claim, and added how to create a Reddit app and configure the credentials (with an MCP client env example), what a 403 means, and the banner note. The package description is updated to match.

Verification

  • Mocked HTTP (tests/test_reddit_oauth.py, httpx.MockTransport, through the real AsyncRedd):
    • the token request has the right method, URL, Basic auth header, grant_type=client_credentials body, and User-Agent;
    • API calls go to oauth.reddit.com/<same path> with bearer <token> and nothing else is contacted;
    • the token is reused across calls and refreshed after expiry (fake clock);
    • a 401 refreshes the token and retries once;
    • token endpoint 401, or a 200 without access_token, raises AuthenticationError;
    • 403, 404, 429, connection errors, and non-JSON bodies on the anonymous path keep their real status or type.
  • Live Reddit over a real stdio session (spawned the server and called reddit_get_subreddit_posts with fastmcp.Client):
    • no credentials: the 403 message above. main returns HTTP error (0). Reddit may be temporarily unavailable...;
    • bogus credentials: Reddit's real token endpoint answers 401 and the server reports the OAuth authentication error;
    • server stderr no longer contains the FastMCP banner (main prints it).
  • PYTHONIOENCODING=cp1252 reddit-no-auth-mcp-server --help works. On main it raises UnicodeEncodeError: 'charmap' codec can't encode character '\U0001f916'.
  • ruff check, ruff format --check, and pytest (81 passed) are clean. mypy src shows only the existing asdict ignore-code error, which fix: return JSON objects from list tools so outputSchema matches the payload #5 fixes.

Not verified: a successful call with valid credentials against oauth.reddit.com, since I had no Reddit app credentials. Specifically untested: that the OAuth host serves redd's .json-suffixed paths (/r/<sub>/hot.json, /user/<name>/.json, /search.json, /<permalink>.json) the same way www does.

Reddit now answers most unauthenticated programmatic requests with
HTTP 403 ("You've been blocked by network security"), and redd reports
every transport failure as HttpError(0), so users only saw
"HTTP error (0). Reddit may be temporarily unavailable".

- Optional REDDIT_CLIENT_ID / REDDIT_CLIENT_SECRET (and REDDIT_USER_AGENT):
  when set, redd's HTTP transport is replaced by an adapter that obtains
  an app-only token (client_credentials grant), refreshes it on expiry or
  401, and sends requests to oauth.reddit.com with a bearer token.
  Anonymous access stays the default; setting only one variable is a
  usage error.
- Recover the real cause behind redd's HttpError(0): HTTP status plus a
  short body summary, 404 -> not found, network errors, non-JSON bodies,
  and OAuth token failures. Tool errors now explain 401/403/429/5xx, and
  403 points to the OAuth credentials.
- Disable the FastMCP startup banner on the stdio transport.
- Remove the emoji from the CLI description; --help crashed on Windows
  cp1252 consoles.
- Read __version__ from package metadata (it was stuck at 0.1.0).
- README: optional credentials, 403 explanation, banner note.
@eliasbiondo
eliasbiondo merged commit 129c682 into main Oct 24, 2026
2 checks passed
@eliasbiondo
eliasbiondo deleted the fix/reddit-oauth-and-stdio branch October 24, 2026 23:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

All tools return HTTP 0 / 403 from Reddit (unauthenticated requests blocked)

1 participant